package org.one.stone.soup.db.storage;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ChunkedDataStore {
	
	private static final int CHUNK_INDEX_SIZE = 8;
	private static final int CHUNK_FLAGS_SIZE = 1;
	private LargeRandomAccessFile store;
	private int chunkSize;
	
	public ChunkedDataStore(String fileName,int chunkSize) throws FileNotFoundException {
		this.chunkSize = chunkSize;
		store = new LargeRandomAccessFile(fileName);
	}
	
	public List<Long> getRecordIndexes() {
		List<Long> records = new ArrayList<Long>();
		
		//Go through chunks looking for byte set as 0x04
		
		return records;
	}
	
	public long store(byte[] data) throws IOException {
		long chunkIndex=-1;
		long lastIndex=-1;
		long index=-1;
		for(index=0;index<data.length;index+=chunkSize) {
			
			if(data.length-index<=chunkSize) {
				break;
			} else {
				if(chunkIndex==-1) {
					lastIndex = writeFirstChunk( cutChunk(data,index) );
				} else {
					lastIndex = writeNextChunk( cutChunk(data,index) );
				}
			}
			if(chunkIndex==-1) {
				chunkIndex = lastIndex;
			}
		}
		lastIndex = writeLastChunk( cutChunk(data,index),data.length );
		if(chunkIndex==-1) {
			chunkIndex = lastIndex;
		}
		
		return chunkIndex;
	}
	
	private byte[] cutChunk(byte[] data,long index) {
		data = pack(data,chunkSize);
		
		return substring(data,index,index+chunkSize);
	}
	
	private long writeLastChunk(byte[] chunk,long dataSize) throws IOException {
		chunk = pack(chunk,chunkSize);
		
		long index = findAvailableChunk(true);
		store.seek(index);
		store.write(chunk);
		
		writeLastChunkHeader(dataSize);
		
		//System.out.prlongln( dumpStore() );
		
		return index;
	}
	
	private long writeNextChunk(byte[] chunk) throws IOException {
		chunk = pack(chunk,chunkSize);
		
		long index = findAvailableChunk(true);
		store.seek(index);
		store.write(chunk);		
		
		long nextChunk = findAvailableChunk(false);
		if(nextChunk==0) {
			nextChunk = getChunkSize();
		}
		writeChunkHeader(nextChunk);
		
		//System.out.prlongln( dumpStore() );
		
		return index;
	}
	
	private long writeFirstChunk(byte[] chunk) throws IOException {
		chunk = pack(chunk,chunkSize);
		
		long index = findAvailableChunk(true);
		store.seek(index);
		store.write(chunk);		
		
		long nextChunk = findAvailableChunk(false);
		if(nextChunk==0) {
			nextChunk = getChunkSize();
		}
		writeFirstChunkHeader(nextChunk);
		
		//System.out.prlongln( dumpStore() );
		
		return index;
	}
	
	private long findAvailableChunk(boolean save) throws IOException {
		long p = store.getFilePointer();
		
		store.seek(0);
		long pointer=0;
		for(pointer=0;pointer<store.length();pointer+=getChunkSize()) {
			if(store.length()<=pointer+getChunkSize()) {
				if(save) {
					store.seek(pointer+getChunkSize()+chunkSize);
					store.writeByte(0x01);
				}
				store.seek(p);
				return pointer+getChunkSize();
			}
			
			store.skipBytes(chunkSize);
			byte mask = store.readByte();
			if((mask & 0x2)!=0) {
				if(save) {
					store.seek(pointer+chunkSize);
					store.writeByte(0x01);
				}
				store.seek(p);
				return pointer;
			}
			store.skipBytes(CHUNK_INDEX_SIZE);
		}
		store.seek(p);
		return pointer;
	}
	
	public byte[] retrieve(long chunkIndex) throws IOException {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		
		store.seek(chunkIndex);
		byte[] chunk = new byte[chunkSize];
		store.read(chunk, chunkSize);
		out.write(chunk);
		
		byte flags = store.readByte();
		while((flags & 0x01)==0 && (flags & 0x02)==0) {
			chunkIndex = store.readLong();
			store.seek(chunkIndex);
			
			store.read(chunk, chunkSize);
			out.write(chunk);
			
			flags = store.readByte();
		}
		if((flags & 0x02)!=0) {
			return null;
		}
		
		long dataSize = store.readLong();
		return substring(out.toByteArray(),0,(long)dataSize);
	}
	
	public boolean remove(long chunkIndex) throws IOException {
		store.seek(chunkIndex+chunkSize);
		store.writeByte(0x02);
		return true;
	}

	private long getChunkSize() {
		return chunkSize + CHUNK_FLAGS_SIZE + CHUNK_INDEX_SIZE;
	}
	
	private void writeFirstChunkHeader(long nextChunk) throws IOException {
		byte mask = 0;
		mask += 4;
		
		store.writeByte(mask);
		store.writeLong(nextChunk);
	}
	
	private void writeChunkHeader(long nextChunk) throws IOException {
		byte mask = 0;
		
		store.writeByte(mask);
		store.writeLong(nextChunk);
	}
	
	private void writeLastChunkHeader(long length) throws IOException {
		byte mask = 0;
		mask += 1;
		
		store.writeByte(mask);
		store.writeLong(length);
	}
	
	public String dumpStore() throws IOException {
		StringBuffer buffer = new StringBuffer("Length: "+store.length()+"\n>> ");
		long p = store.getFilePointer();
		store.seek(0);
		for(long index=0;index<store.length();index++) {
			byte b = store.readByte();
			buffer.append( Integer.toString(b)+"("+(char)b+") " );
			if(index>0 && index%80==0) {
				buffer.append("\n>> ");
			}
		}
		store.seek(p);
		return buffer.toString();
	}
	
	private byte[] substring(byte[] data,long startIndex,long endIndex) {
		if(endIndex>data.length) {
			endIndex = data.length;
		}
		
		long length = endIndex-startIndex;
		if(startIndex==0 && data.length==length) {
			return data;
		}
		
		byte[] substring = new byte[(int) length];
		
		System.arraycopy(data, (int)startIndex, substring, 0, (int)length);
		
		return substring;
	}
	
	private byte[] pack(byte[] data,long length) {
		if(data.length>=length) {
			return data;
		}
		
		byte[] chunk = new byte[(int)length];
		System.arraycopy(data, 0, chunk, 0, data.length);
		return chunk;
	}
}
